/**
 * 
 */
package com.google.code.jbauble.dao.mapping;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.code.jbauble.dao.annotations.Entity;
import com.google.code.jbauble.dao.annotations.Id;
import com.google.code.jbauble.dao.annotations.Transient;
import com.google.code.jbauble.exception.MappingException;
import com.google.code.jbauble.utils.ReflectionUtils;

/**
 * Represents a mapped class between the MongoDB DBObject and the java POJO.
 * 
 * This class will validate classes to make sure they meet the requirement for persistence.
 * 
 * @author Scott Hernandez
 */
@SuppressWarnings("unchecked")
public class MappedClass {
	/** special fields representing the Key of the object */
	private Field idField;
	
	/** the collectionName based on the type and @Entity value(); this can be overridden by the @CollectionName field on the instance*/
	private String collName;
	
	/** a list of the fields to map */
	private List<MappedField> persistenceFields = new ArrayList<MappedField>();
	
	/** the type we are mapping to/from */
	private Class<?> clazz;
	Mapper mapr;
	
	/** constructor */
	public MappedClass(Class<?> clazz, Mapper mapr) {
		this.mapr = mapr;
		this.clazz = clazz;

		buildMappedFields();
	}
	
	protected void buildMappedFields() {
		Entity e = ReflectionUtils.getAnnotation(clazz, Entity.class);
		collName = (e != null && !e.value().equals("")) ? e.value() : clazz.getSimpleName();
		
		for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(clazz, true)) {
			field.setAccessible(true);
			int fieldMods = field.getModifiers();
			try {
				if (field.isAnnotationPresent(Transient.class))
					continue;
				else if ( field.isSynthetic() && (fieldMods & Modifier.TRANSIENT) == Modifier.TRANSIENT )
					continue;
				else if (field.isAnnotationPresent(Id.class)) {
						MappedField mf = new MappedField(field, clazz);
						persistenceFields.add(mf);
						idField = field;
				} else if (	isSupportedType(field.getType()) ||
							ReflectionUtils.implementsInterface(field.getType(), Serializable.class)) {
					persistenceFields.add(new MappedField(field, clazz));
				}
			} catch (MappingException e1) {
				e1.printStackTrace();
			}
		}
	}
	
	@Override
	public String toString() {
		return "MappedClass - kind:" + this.getCollectionName() + " for " + this.getClazz().getName() + " fields:" + persistenceFields;
	}
	
	/** Returns fields annotated with the clazz */
	public List<MappedField> getFieldsAnnotatedWith(Class<? extends Annotation> clazz){
		List<MappedField> results = new ArrayList<MappedField>();
		for(MappedField mf : persistenceFields){
			if(mf.foundAnnotations.containsKey(clazz))
				results.add(mf);
		}
		return results;
	}
	
	/** Returns the MappedField by the name that it will stored in mongodb as*/
	public MappedField getMappedField(String storedName) {
		for(MappedField mf : persistenceFields)
			for(String n : mf.getLoadNames())
				if (storedName.equals(n)) 
					return mf;
		
		return null;
	}
	
	/** Check java field name that will stored in mongodb */
	public boolean containsJavaFieldName(String name) {
		return getMappedField(name)!=null;
	}
	/** Returns MappedField for a given java field name on the this MappedClass */
	public MappedField getMappedFieldByJavaField(String name) {
		for(MappedField mf : persistenceFields)
			if (name.equals(mf.getJavaFieldName())) return mf;
		
		return null;
	}
	
	/** Checks to see if it a Map/Set/List or a property supported by the MangoDB java driver*/
	public static boolean isSupportedType(Class<?> clazz) {
		if (ReflectionUtils.isPropertyType(clazz)) return true;
		if (clazz.isArray() || Map.class.isAssignableFrom(clazz) || Iterable.class.isAssignableFrom(clazz)){
			Class<?> subType = null;
			if (clazz.isArray()) subType = clazz.getComponentType();
			else subType = ReflectionUtils.getParameterizedClass(clazz);
			
			//get component type, String.class from List<String>
			if (subType != null && subType != Object.class && !ReflectionUtils.isPropertyType(subType))
				return false;
			
			//either no componentType or it is an allowed type
			return true;
		}
		return false;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Class<?>) return equals((Class<?>)obj);
		else if (obj instanceof MappedClass) return equals((MappedClass)obj);
		else return false;
	}
	
	public boolean equals(MappedClass clazz) {
		return this.getClazz().equals(clazz.getClazz());
	}
	
	public boolean equals(Class<?> clazz) {
		return this.getClazz().equals(clazz);
	}
	
	/** @return the idField */
	public Field getIdField() {
		return idField;
	}
	
	/**
	 * @return the persistenceFields
	 */
	public List<MappedField> getPersistenceFields() {
		return persistenceFields;
	}
	
	/**
	 * @return the collName
	 */
	public String getCollectionName() {
		return collName;
	}
	
	/**
	 * @return the clazz
	 */
	public Class<?> getClazz() {
		return clazz;
	}
	
	/** @return the Mapper this class is bound to */
	public Mapper getMapper(){
		return mapr;
	}

	public MappedField getMappedIdField() {
		return getFieldsAnnotatedWith(Id.class).get(0);
	}
	
}